Mines 2023 - La typographie informatisée¶

Partie I - Préambule¶

Q1¶

On convertit 100 en base 16 : $0*1+0*16*1*16^2 = 256$ ; cela fait 2.56$

Q2¶

In [12]:
v = [[[0.25,1.0],[0.25,-1.0],[0.0,-1.0]],[[0.25,1.25]]]
In [4]:
v[0]
Out[4]:
[[0.25, 1.0], [0.25, -1.0], [0.0, -1.0]]
In [5]:
import matplotlib.pyplot as plt
In [6]:
plt.plot([0.25,0.25,0],[1,-1,-1])
plt.scatter([0.25],[1.25],1)  # pour faire le point tout seul sinon on ne le voit pas
plt.grid()
plt.axis('equal');
No description has been provided for this image

Partie II - Gestion de polices de caractères vectorielles¶

Q3¶

On veut compter le nombre de glyphe en Roman : on compte le nombre d'enregistrements dans la table Glyphe pour lesquels l'attribut groman est True :

In [ ]:
SELECT COUNT(*)
FROM Glyphe
WHERE groman = True

Q4¶

On peut soit supposer qu'on connait le code du caractère 'A' (65) : on a besoin de la table Police pour récupérer le pid dont le pnom est Helvetica et de la table Glyphe pour récupérer le contenu de gdesc

In [ ]:
SELECT desc 
FROM Glyphe
JOIN Police ON Glyphe.pid=Police.pid
WHERE
pnom = "Helvetica" AND code=65 AND Glyphe.Roman=False

Si on ne veut pas connaître le code du caractère 'A', il faut en plus le récupérer dans la table Caractere ce qui ajoute une jonction supplémentaire :

In [ ]:
SELECT desc
FROM Glyphe 
JOIN Police ON Glyphe.pid=Police.pid
JOIN Caractere ON Caractere.code=Glyphe.code
WHERE
pnom = "Helvetica" AND car='A' AND Glyphe.Roman=False

Q5¶

On utilise les deux tables Famille et Police : on effectue un regroupement par paquets sur le fid de Famille, on ne conserve que celles qui ont un nombre de pid non nul, et on trie :

In [ ]:
SELECT fnom, COUNT(pid) as nombre
FROM Famille
JOIN Police ON Police.fid=Famille.fid
GROUP BY fnom
HAVING nombre>0
ORDER BY fnom

Partie II - Manipulation de descriptions vectorielles de glyphes¶

Q6¶

In [ ]:
def points(v):
    L = []              # liste des points
    for ml in v:        # on parcourt les multi-lignes
        for point in ml:
            L.append(point)
    return L        
In [ ]:
points(v)

Q7¶

In [ ]:
def dim(l,n):
    L = []
    for x in l:
        L.append(x[n])
    return L
In [ ]:
l = [[1,2],[3,4],[5,6],[7,8]]
dim(l,1)
In [ ]:
dim(l,0)

Q8¶

On récupère tous les points du glyphe, puis les premières coordonnées. On n'a plus qu'à prendre les extrema

In [ ]:
def largeur(v):
    p = points(v)
    abscisses = dim(p,0)
    return max(abscisses)-min(abscisses)    
In [ ]:
largeur(v)

Q9¶

In [ ]:
def obtention_margeur(police):
    L = []
    debut = ord('a')
    for car in range(debut,debut+26):
        L.append(largeur(glyphe(chr(car),police,True)))
        L.append(largeur(glyphe(chr(car),police,False)))
    return L        

Q10¶

Comme indiqué, la fonction f s'applique aux points - on doit donc parcourir les différentes multi-lignes du glyphe et appliquer la fonction à chacun de ses points

In [ ]:
def transforme(f,v):
    L = []
    for ml in v:
        nouvelle_ml = []
        for p in ml:
            nouvelle_ml.append(f(p))
        L.append(nouvelle_ml)
    return L

On peut aussi utiliser la construction de liste par compréhension (qui réalise la même chose que parcourir une liste et ajouter à la nouvelle liste)

In [ ]:
def transforme(f,v):
    v2 = [[f(p) for p in ml] for ml in v]
    return v2

Q11¶

L'application zzz est l'application $(x,y)\mapsto (0.5*x,y)$. Les abscisses sont divisées par $2$, les ordonnées ne changent pas. On divise la largeur du glyphe par $2$

Q12¶

In [ ]:
def penche(v):
    def f(p):
        x,y = p
        return [x+0.5*y,y]
    return transforme(f,v)

IV Rasterisation¶

In [ ]:
from PIL import Image

im = Image.new("1", (50, 100), color=1)
for y in range(60, 65):
    for x in range(5, 45):
        im.putpixel((x, y), 0)
        im.putpixel((x, y-20), 0)
im.save("egal.png")
plt.imshow(im)
In [8]:
from math import floor

def trace_quadrant_est(im, p0, p1):
    x0, y0 = p0
    x1, y1 = p1
    dx, dy = x1-x0, y1-y0
    im.putpixel(p0, 0)
    for i in range(1, dx):
        p = (x0 + i, y0 + floor(0.5 + dy * i / dx))
        im.putpixel(p, 0)
    im.putpixel(p1, 0)
im = Image.new("1", (10, 10), color=1)
trace_quadrant_est(im, (0, 0), (6, 2))
trace_quadrant_est(im, (9, 8), (1, 9))
trace_quadrant_est(im, (3, 0), (5, 8))
plt.imshow(im);  # à la place du im.show() qui affiche ailleurs
No description has been provided for this image

Pour la ligne 15: on trace une ligne entre les points de coordonnées (0,0) et (6,2) :

  • dx:=6 et dy:=2
  • on trace le premier point de coordonnées (0,0)
  • la boucle sur i va de 1 à 5 (compris) pour les 5 points intermédiaires, on obtient les points (1,0), (2,1), (3,1), (4,1), (5,2)
  • on place le dernier point (6,2)

Q14¶

Cette fois on a dx=-8 donc aucun passage dans la boucle for ; on a les deux points extrêmes qui apparaissent (placés en dehors de la boucle) et c'est tout. Le problème provient donc qu'on doit reculer

Q15¶

On a dx=2 ; à part les 2 points aux extrémités, il n'y a qu'un passage dans la boucle et ainsi un seul point intermédiaire. La pente est trop grande (plus que $1$ en valeur absolue et on oublie des points)

Q16¶

In [9]:
def trace_quadrant_sud(im, p0, p1):
    x0, y0 = p0
    x1, y1 = p1
    dx, dy = x1-x0, y1-y0
    im.putpixel(p0, 0)
    for j in range(1, dy):
        p = (x0 + floor(0.5+dx*j / dy), y0 + j)
        im.putpixel(p, 0)
    im.putpixel(p1, 0)
im = Image.new("1", (10, 10), color=1)
trace_quadrant_sud(im, (3, 0), (5, 8))
plt.imshow(im);  # à la place du im.show() qui affiche ailleurs
No description has been provided for this image

Q17¶

In [10]:
def trace_segment(im,p0,p1):
    x0, y0 = p0
    x1, y1 = p1
    dx, dy = x1-x0, y1-y0
    if abs(dy)<abs(dx):      # |dy/dx|<1 - on est plus proche de l'horizontale
        if x0<x1:            # p0 est à droite de p1
            trace_quadrant_est(im,p0,p1)
        else:                # p1 est à droite de p0
            trace_quadrant_est(im,p1,p0)
    else:                    # plutôt vertical
        if y0<y1:
            trace_quadrant_sud(im,p0,p1)
        else:
            trace_quadrant_sud(im,p1,p0)
In [11]:
# on teste... tout est multiplié par 100 pour avoir une meilleure résolution
im = Image.new("1", (1000, 1000), color=1)
trace_segment(im, (0, 0), (600, 200))
trace_segment(im, (900, 800), (100, 900))
trace_segment(im, (300, 0), (500, 800))
trace_segment(im, (800,300), (100,600))
# test pour un point unique
trace_segment(im, (250,250),(250,250))
plt.imshow(im);
No description has been provided for this image

Partie V - Affichage du texte¶

Q18¶

In [6]:
from math import floor

def position(p,pz,taille):
    centre_x,centre_y = pz # coordonnées du point (0,0) dans le repère de l'écran/de l'image
    x,y = p # coordonnées normalisées
    pos_x = floor(centre_x + taille*x)
    pos_y = floor(centre_y - taille*y)
    return pos_x,pos_y
    
In [7]:
position((1,1),(100,100),1)
Out[7]:
(101, 99)

Q19¶

In [8]:
def affiche_car(page, c, police,roman, pz, taille):
    v = glyphe(c, police, roman)                  # récupère la liste de listes de points
    for ml in v:                                  # on parcourt les multilignes
        point = ml[0]                             # point de départ
        cx,cy = position(point,pz,taille)         # point de départ dans le repère de l'image
        g,d = cx,cx                               # valeurs les plus à gauche et à droite
        trace_segment(page,[cx,cy],[cx,cy])       # on trace le premier point (au cas où il serait tout seul)
        for k in range(1,len(ml)):
            cx2,cy2 = position(ml[k],pz,taille)   # nouveau point
            g = min(g,cx2)
            d = min(d,cx2)
            trace_segment(page,[cx,cy],[cx2,cy2]) # segment entre l'ancien et le nouveau point
            cx,cy = cx2,cy2                       # le nouveau point devient le prochain départ 
    return d-g+1
    # return taille*largeur(v)
            

Q20¶

In [9]:
def affiche_mot(page, mot, ic, police, roman, pz, taille):
    x,y = pz 
    for c in mot:
        largeur = affiche_car(page, c, police, roman, [x,y], taille)
        x += largeur+ic  # nouvelle abscisse de départ
    return x-largeur-ic  # on recule de l'espace qu'on a ajouté

Partie VI - Justification d'un paragraphe¶

Q21¶

In [4]:
def glouton(Lmots:[int],L:int)->[[int]]:
    lignes=[]
    nligne=[]
    l=0
    for c in Lmots :
        if (c + l) > L:
            lignes.append(nligne)
            nligne=[c]
            l=c+1
        else:
            l=l+c+1
            nligne.append(c)
    lignes.append(nligne)
    return lignes
In [5]:
glouton([3,7,5,4,8,6,3,4,2,6,4,3,5],20)
Out[5]:
[[3, 7, 5], [4, 8, 6], [3, 4, 2, 6], [4, 3, 5]]

Le principe :

  • on ajoute des mots tant qu'on ne dépasse pas la taille de la ligne ; la variable l désigne le nombre de caractères dans la ligne actuelle
  • si l'ajout fait déborder (ligne 6 : l+c > L) alors on ajoute la ligne actuelle à la liste des lignes, on repart d'une nouvelle ligne avec le mot en train d'être placé
  • sinon on ajoute la taille du mot + 1 pour l'espace (ligne 11)

On dit que l'algorithme est glouton car il prend les mots les uns après les autres et le place sans regarder globalement s'il n'y aurait pas un meilleur équilibre (c'est une optimisation locale et pas globale)

In [6]:
# test sur l'exemple proposé
lmots = [2,4,2,6,6]
glouton(lmots,10)
Out[6]:
[[2, 4, 2], [6], [6]]
In [7]:
def cout(i:int,j:int,lmots:[int],L:int)->int:
    res=sum(lmots[i:j+1])+(j-i)
    if res>L:
        return float("inf")
    else:
        return (L-res)**2

Q22¶

On regarde sur les deux situations :

  • algorithme glouton :
    • les séparations sont i=0,j=2 puis i=j=3 et enfin i=j=4
    • les coûts sur chaque ligne : $(10-2-8)^2$, $(10-0-6)^2$ et $(10-0-6)^2$
    • ce qui donne un coût total de $32$
  • seconde répartition :
    • les séparations sont i=0,j=1 puis i=2,j=3 et enfin i=j=4
    • les coûts sur chaque ligne : $(10-1-6)^2$, $(10-1-8)^2$ et $(10-0-6)^2$
    • ce qui donne un coût total de $26$

Q23¶

L'idée pour mémoïser est de stocker les valeurs de $d$ qu'on a déjà trouvées. Lorsqu'on a besoin de calculer une valeur de $d$, on regarde si on la connaît déjà (l'algorithme renvoie cette valeur) et sinon on la calcule. On stocke tout cela dans un dictionnaire memo :

In [26]:
memo = {len(lmots) : 0}

def progd_memo(i,lmots,L, memo):
    if i in memo:
        return memo[i]
    else:
        mini =float("inf")
        for j in range(i+1,len(lmots)+1):
            d = progd_memo(j,lmots,L,memo)+cout(i,j-1,lmots,L)
            if d<mini:
                mini = d
        memo[i] = mini
        return mini
In [29]:
progd_memo(0,lmots,10,memo)
Out[29]:
26
In [30]:
memo
Out[30]:
{5: 0, 4: 16, 3: 32, 2: 17, 1: 41, 0: 26}

Comment comprendre ces valeurs :

  • 5:0 -> pas de mot à placer
  • 4:16 -> le coût le plus faible pour placer à partir du mot 4 (il n'y en a plus qu'un) est de le placer seul (taille $6$, cout de $16$)
  • 3:32 -> idem avec les mots 3 et 4 (chacun sur une ligne, cout $16+16=32$
  • 2:17 -> mots 2,3 et 4 ; 2 lignes (2+' '+6 : cout $1$, et le dernier mot seul : coût $16$)
  • 1:41 -> ...
  • 0:26 -> on places les mots de 0 à 4 ; meilleur coût à $26$

Q24¶

¶

La fonction cout(i,j) demande $1+1+1+(j-i+1)+1=j-i+5$ opérations d'additions ou multiplications (le dernier $+1$ est pour le carré sauf s'il n'y est pas)

Si on note $C(i,n)$ le coût pour calculer $d(i)$ par la formule récursive pour $n$ mots (c'est-à-dire qu'on refait le calcul des $d(j)$ suivants), on obtient une relation $$ C(i,n) = \displaystyle\sum_{j=i+1}^n \left(C(j)+f(i,j)\right)$$ où $f(i,j)$ est le nombre d'opérations dans le calcul de cout(i,j-1), donc au moins $j-i+4$

Si on note $D(n)$ la complexité pour le placement pour $n$ mots, on a une minoration très très large $D_{n+2}\geqslant D_{n+1}+D_n$ et donc une croissance exponentielle

Pour la version progd_bashaut : on a deux boucles imbriquées (avec au plus $n$ étapes chacunes), à l'intérieur la complexité principale est le calcul de la fonction cout (+1 addition), donc en $O(n)$. Finalement l'algorithme est de complexité $O(n^3)$.

In [8]:
def progd_bashaut(lmots:[int],L:int,t:[int])->int:
    M=[0]*(len(lmots)+1)
    for i in range(len(lmots)-1,-1,-1):
        mini,indi=float("inf"),-1
        for j in range(i+1,len(lmots)+1):
            d=M[j]+cout(i,j-1,lmots,L)
            if d<mini:
                mini,indi=d,j
        t[i]=indi
        M[i]=mini
    return M[-1]
In [13]:
t = [0]*(len(lmots))
progd_bashaut(lmots,10,t)
t
Out[13]:
[2, 3, 4, 4, 5]

Q25¶

In [20]:
def lignes(mots,t,L):
    Liste = []
    n = len(mots)
    depart = 0                    # numéro du mot actuel (début d'une ligne)
    while depart<n:
        fin = t[depart]           # c'est le niveau où on doit couper
        s_L = mots[depart:fin]    # on crée la liste des mots entre ces deux positions (la seconde est exclu, ça commence la ligne suivante)
        Liste.append(s_L)         # on ajoute cette liste à la liste des mots d'une ligne
        depart = fin              # numéro du mot pour la ligne suivante
    return Liste
In [21]:
t=[2, 3, 5, 5, 5, 6, 7, 9, 11, 11, 11]
L=15
mots=["Lorem","ipsum","dolor","sit","amet,","consectetur","adipiscing",
"elit.","Sed","non","risus."]
In [24]:
A = lignes(mots,t,L) ; A
Out[24]:
[['Lorem', 'ipsum'],
 ['dolor', 'sit', 'amet,'],
 ['consectetur'],
 ['adipiscing'],
 ['elit.', 'Sed'],
 ['non', 'risus.']]

Q26¶

In [28]:
def formatage(lignedemots,L):
    texte_formaté = ""
    for ligne in lignedemots:
        longueur = 0
        ligne_actuelle = ""
        for m in ligne:
            longueur += len(m)
        n_mots = len(ligne)
        cases = n_mots-1       # nombre de trous à remplir
        restant = L-longueur   # nombre d'espaces à placer
        if len(ligne)==1:
            ligne_actuelle = ligne[0]+restant*' '
        else:
            deja_fait = 0
            ligne_actuelle = ligne[0]
            for j in range(1,n_mots):
                espaces_depuis_debut = j*restant//cases
                nouveaux_espaces = espaces_depuis_debut-deja_fait
                ligne_actuelle += " "*(nouveaux_espaces)+ligne[j]
                deja_fait = espaces_depuis_debut
        texte_formaté += ligne_actuelle+"\n"
    return texte_formaté
In [32]:
print(formatage(A,15))
Lorem     ipsum
dolor sit amet,
consectetur    
adipiscing     
elit.       Sed
non      risus.

In [ ]: